/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2011, Salzburg Research. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * - Neither the name of the LMF library client Project nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Contributor(s):
 * Stephanie Stroka
 *
 * File Info:
 * User: Stephanie Stroka
 * Date: 2011/08/19
 * Time: 18:04
 *
 * Project: lmflib
 */

package kiwi.facading.services;

import kiwi.config.ConfigurationServiceImpl;
import kiwi.config.api.ConfigurationService;
import kiwi.connection.LMFClientImpl;
import kiwi.connection.api.LMFClient;
import kiwi.facading.api.LiteralService;
import kiwi.facading.api.TripleStore;
import kiwi.facading.model.KiWiTriple;
import kiwi.facading.model.resources.*;
import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.openrdf.model.Statement;
import org.openrdf.repository.Repository;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.RepositoryResult;
import org.openrdf.repository.sail.SailRepository;
import org.openrdf.rio.RDFFormat;
import org.openrdf.rio.RDFParseException;
import org.openrdf.sail.memory.MemoryStore;
import org.openrdf.sail.memory.model.MemURI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * TripleStoreImpl implements the TripleStore interface.
 * The service is used to create triples on an LMF server,
 * to list them and to remove them from the LMF server.
 *
 * TripleStoreImpl interacts with the LMF server via HTTP commands.
 *
 *
 * @author Stephanie Stroka
 * User: Stephanie Stroka
 *       (stephanie.stroka@salzburgresearch.at)
 * Date: 01.06.2011
 * Time: 13:51:32
 */
public class TripleStoreImpl implements TripleStore {

    private Logger log =  LoggerFactory.getLogger(this.getClass());

    private ConfigurationService configurationService;

    private LiteralService literalService;

    private LMFClient lmfClient;

    public TripleStoreImpl() {
        configurationService = new ConfigurationServiceImpl();
        literalService = new LiteralServiceImpl();
        lmfClient = new LMFClientImpl();
    }

    /**
     * Create a KiWi triple on/from the LMF server using the provided subject, predicate, and
     * object.
     *
     * @param subject the subject node of an RDF triple
     * @param property the property node of an RDF triple
     * @param object the object node of an RDF triple
     * @param space the knowledge space of an RDF triple
     * @return the newly constructed triple
     */
    public synchronized KiWiTriple createTriple(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource space) {

        KiWiUriResource _subject;

        if(!subject.isUriResource()) {
            log.error("The LMF client does not support anonymous resources at the moment. ");
            return null;
        } else {
            _subject = (KiWiUriResource) subject;
        }

        KiWiTriple triple = new KiWiTriple(subject, property, object, space);

        URI uri = configurationService.getResourceConnection(_subject);

        /*
        curl -X PUT -i -H "Content-Type: test/rdf+n3; rel=meta"  \
             -d '<http://localhost:8080/LMF/resource/1234> <http://www.w3.org/2000/01/rdf-schema#label> "Test Resource".' \
             http://localhost:8080/LMF/resource/1234
         */
        HttpClient httpClient = new DefaultHttpClient();
        HttpPut put = new HttpPut(uri);
        put.setHeader("Content-Type", "text/rdf+n3; rel=meta");


        Set<KiWiTriple> existing_triples = listTriples(_subject);
        existing_triples.add(triple);

        try {

            log.debug("Preparing content:");
            StringBuilder req_str = new StringBuilder();
            for(KiWiTriple t : existing_triples) {
                log.debug("{} . ", t.toN3());
                req_str.append(t.toN3());
                req_str.append(" . ");
            }

            put.setEntity(new StringEntity(req_str.toString()));
            HttpResponse response = httpClient.execute(put);

            int status = response.getStatusLine().getStatusCode();

            // Is the server sending a redirect request? 
            if(status >=300 && status < 400) {
                String redirectLocation;
                Header locationHeader = response.getFirstHeader("location");
                if (locationHeader != null) {
                    log.info("location: {}", locationHeader.getValue());
                    redirectLocation = locationHeader.getValue();
                } else {
                // The response is invalid and did not provide the new location for
                // the resource.  Report an error or possibly handle the response
                // like a 404 Not Found error.
                    log.error("Location not found");
                    return null;
                }
                URI redirect_uri = new URI(redirectLocation);
                HttpPut redirect_put = new HttpPut(redirect_uri);
                redirect_put.setEntity(new StringEntity(req_str.toString()));
                response = httpClient.execute(redirect_put);
            }

            status = response.getStatusLine().getStatusCode();

            InputStream is = response.getEntity().getContent();

            log.debug("status: {}", status);
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(is));

            if(status != 200) {
                log.error("Could not create triple: {}", response.getStatusLine().getReasonPhrase());
                return null;
            }

            StringBuilder response_msg = new StringBuilder();
            String s;
            while((s = in.readLine()) != null) {
                response_msg.append(s);
            }
            log.debug("response: {}", response_msg.toString());
            
        } catch (IOException e) {
            e.printStackTrace();
        } catch (HttpException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        return triple;
    }

    /**
     * Removes a triple from a resource.
     * The method retrieves all outgoing triples for a given resource,
     * filters the triple that has been provided as a parameter and
     * sends the remaining triples back to the LMF server via HTTP PUT.
     *
     * @param triple the triple to be removed
     */
    @Override
    public void removeTriple(KiWiTriple triple) {
        KiWiUriResource subject;

        if(!triple.getSubject().isUriResource()) {
            log.error("The LMF client does not support anonymous resources at the moment. ");
            return;
        } else {
            subject = (KiWiUriResource) triple.getSubject();
        }

        URI uri = configurationService.getResourceConnection(subject);

        /*
        curl -X PUT -i -H "Accept: text/rdf+n3;rel=meta"  \
             -d '<http://localhost:8080/LMF/resource/1234> <http://www.w3.org/2000/01/rdf-schema#label> "Test Resource".' \
             http://localhost:8080/LMF/resource/1234
         */
        HttpClient httpClient = new DefaultHttpClient();
        HttpPut put = new HttpPut(uri);
        put.setHeader("Content-Type", "text/rdf+n3; rel=meta");

        Set<KiWiTriple> existing_triples = listTriples(subject);
        if(!existing_triples.remove(triple)) {
            log.warn("No triple removed.");
            return;
        }

        try {

            log.debug("Preparing content:");
            StringBuilder req_str = new StringBuilder();
            for(KiWiTriple t : existing_triples) {
                log.debug("{} . ", t.toN3());
                req_str.append(t.toN3());
                req_str.append(" . ");
            }

            put.setEntity(new StringEntity(req_str.toString()));
            HttpResponse response = httpClient.execute(put);

            int status = response.getStatusLine().getStatusCode();

            InputStream is = response.getEntity().getContent();

            log.debug("status: {}", status);
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(is));

            StringBuilder response_msg = new StringBuilder();
            String s;
            while((s = in.readLine()) != null) {
                response_msg.append(s);
            }
            log.debug("response: {}", response_msg.toString());

        } catch (IOException e) {
            e.printStackTrace();
        } catch (HttpException e) {
            e.printStackTrace();
        }
    }

    /**
     * @see #removeTriple(KiWiTriple triple) 
     * @param subject the subject node of the RDF triple
     * @param property the property node of the RDF triple
     * @param object the object node of the RDF triple
     * @param context the knowledge space of the RDF triple
     */
    @Override
    public void removeTriple(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context) {
        KiWiTriple triple = new KiWiTriple(subject, property, object, context);
        removeTriple(triple);
    }

    /**
     * @see #listTriples(kiwi.facading.model.resources.KiWiResource,
     *                   kiwi.facading.model.resources.KiWiUriResource,
     *                   kiwi.facading.model.resources.KiWiNode,
     *                   kiwi.facading.model.resources.KiWiResource) 
     * @param subject the subject node of the RDF triple
     * @return a set of RDF triples that matches the given parameters for subject nodes
     */
    @Override
    public Set<KiWiTriple> listTriples(KiWiResource subject) {
        return listTriples(subject, null, null, null);
    }

    /**
     * @see #listTriples(kiwi.facading.model.resources.KiWiResource,
     *                   kiwi.facading.model.resources.KiWiUriResource,
     *                   kiwi.facading.model.resources.KiWiNode,
     *                   kiwi.facading.model.resources.KiWiResource)
     * @param subject the subject node of the RDF triple
     * @param context the knowledge space of the RDF triple
     * @return a set of RDF triples that matches the given parameters for subject and context nodes
     */
    @Override
    public Set<KiWiTriple> listTriples(KiWiResource subject, KiWiResource context) {
        return listTriples(subject, null, null, context);
    }

    /**
     * @see #listTriples(kiwi.facading.model.resources.KiWiResource,
     *                   kiwi.facading.model.resources.KiWiUriResource,
     *                   kiwi.facading.model.resources.KiWiNode,
     *                   kiwi.facading.model.resources.KiWiResource)
     * @param subject the subject node of the RDF triple
     * @param property the property node of the RDF triple
     * @param context the knowledge space of the RDF triple
     * @return a set of RDF triples that matches the given parameters for subject, property and context nodes
     */
    @Override
    public Set<KiWiTriple> listTriples(KiWiResource subject, KiWiUriResource property, KiWiResource context) {
        return listTriples(subject, property, null, context);
    }

    /**
     * Lists all triples the match the given parameters.
     * To retrieve the triples, a HTTP GET request is send to the LMF server
     * with an Accept header that specifies that RDF triples in N3 format are
     * requested.
     * The received triples (serialized in N3) are imported into a temporary
     * Sesame triplestore and converted into KiWiTriples. 
     *
     * @param subject the subject node of the RDF triple
     * @param property the property node of the RDF triple
     * @param object the object node of the RDF triple
     * @param context the knowledge space of the RDF triple
     * @return a set of RDF triples that matches the given parameters for subject, property, object and context nodes
     */
    @Override
    public Set<KiWiTriple> listTriples(KiWiResource subject, KiWiUriResource property,
                                       KiWiNode object, KiWiResource context) {
        KiWiUriResource _subject;

        if(!subject.isUriResource()) {
            log.error("The LMF client does not support anonymous resources at the moment. ");
            return Collections.emptySet();
        } else {
            _subject = (KiWiUriResource) subject;
        }

        InputStream is = lmfClient.getResource(_subject, LMFClient.DATATYPE.meta, LMFClient.MIMETYPE.n3);

        if(is == null) {
            log.error("getResource failed");
            return new HashSet<KiWiTriple>();
        }

        Repository myRepository = new SailRepository(new MemoryStore());
        try {
            try {
                myRepository.initialize();
                RepositoryConnection repCon = myRepository.getConnection();
                repCon.add(is, ((KiWiUriResource) subject).getUri(), RDFFormat.N3);
                repCon.commit();

                RepositoryResult<Statement> statements = repCon.getStatements(null, null, null, true);
                Set<KiWiTriple> triples = new HashSet<KiWiTriple>();
                while(statements.hasNext()) {
                    Statement statement = statements.next();
                    String sub_str = statement.getSubject().stringValue();
                    log.debug("sub str: {}", sub_str);
                    KiWiResource subject_resource = new KiWiUriResource(sub_str);

                    String prop_str = statement.getPredicate().stringValue();
                    log.debug("prop str: {}", sub_str);
                    KiWiUriResource property_resource = new KiWiUriResource(prop_str);

                    KiWiNode object_node;

                    log.debug("obj class: {}", statement.getObject().getClass());
                    if(statement.getObject().getClass().equals(MemURI.class)) {
                        object_node = new KiWiUriResource(statement.getObject().stringValue());
                        log.debug("obj uri: {}", statement.getObject().stringValue());
                    } else {
                        object_node = literalService.createLiteral(statement.getObject().stringValue());
                        log.debug("obj str: {}", statement.getObject().stringValue());
                    }

                    KiWiUriResource context_resource = null;
                    if(statement.getContext() != null) {
                        String context_str = statement.getContext().stringValue();
                        log.debug("context str: {}", context_str);
                        context_resource = new KiWiUriResource(context_str);
                    }

                    KiWiTriple t = new KiWiTriple(subject_resource, property_resource, object_node, context_resource);
                    if(context == null || context.equals(context_resource)) {
                        if(property == null || property.equals(property_resource)) {
                            if(object == null || object.equals(object_node)) {
                                triples.add(t);
                            }
                        }
                    }
                }
                repCon.close();
                myRepository.shutDown();

                return triples;
            } catch (RepositoryException e) {
                e.printStackTrace();
            } catch (RDFParseException e) {
                e.printStackTrace();
            } finally {
                is.close();
            }
        } catch(IOException e) {
            e.printStackTrace();
        }


        return new HashSet<KiWiTriple>();
    }

    /**
     * @see #hasTriple(kiwi.facading.model.resources.KiWiResource,
     *                 kiwi.facading.model.resources.KiWiUriResource,
     *                 kiwi.facading.model.resources.KiWiNode,
     *                 kiwi.facading.model.resources.KiWiUriResource) 
     * @param subject the subject node of an RDF triple
     * @param property  the property node of an RDF triple
     * @param object  the object node of an RDF triple
     * @return true if the triple exists, false if not
     */
    @Override
    public boolean hasTriple(KiWiResource subject, KiWiUriResource property, KiWiNode object) {
        return hasTriple(subject, property, object, null);
    }

    /**
     * Checks whether a triple exists on the LMF server or
     * whether it does not exist. Therefore, the outgoing triples
     * of the given subject resource are retrieved and
     * scanned for the requested parameters
     * 
     * @param subject the subject node of an RDF triple
     * @param property  the property node of an RDF triple
     * @param object  the object node of an RDF triple
     * @param context the knowledge space of an RDF triple
     * @return true if the triple exists, false if not
     */
    @Override
    public boolean hasTriple(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context) {
        KiWiUriResource _subject;

        if(!subject.isUriResource()) {
            log.error("The LMF client does not support anonymous resources at the moment. ");
            return false;
        } else {
            _subject = (KiWiUriResource) subject;
        }

        KiWiTriple wanted_triple = new KiWiTriple(subject, property, object, context);

        URI uri = configurationService.getResourceConnection(_subject);

        /*
        curl -X GET  -L -i -H "Accept: application/json; rel=meta" \
             http://localhost:8080/LMF/resource/1234
         */
        HttpClient httpClient = new DefaultHttpClient();
        HttpGet get = new HttpGet(uri);
        get.setHeader("Accept", "text/rdf+n3;rel=meta");

        try {
            HttpResponse response = httpClient.execute(get);

            int status = response.getStatusLine().getStatusCode();
            log.debug("status: {}", status);

            if(status < 200 || status >= 400) {
                return false;
            }
            if(status >= 300 && status < 400) {
                String redirectLocation;
                Header locationHeader = response.getFirstHeader("location");
                if (locationHeader != null) {
                    log.info("location: {}", locationHeader.getValue());
                    redirectLocation = locationHeader.getValue();
                } else {
                // The response is invalid and did not provide the new location for
                // the resource.  Report an error or possibly handle the response
                // like a 404 Not Found error.
                    log.error("Location not found");
                    return false;
                }
                URI redirect_uri = new URI(redirectLocation);
                HttpGet redirect_get = new HttpGet(redirect_uri);
                response = httpClient.execute(redirect_get);

                status = response.getStatusLine().getStatusCode();
                log.info("status: {}, reason phrase: {}", status, response.getStatusLine().getReasonPhrase());

                // If the status is not OK, the resource does not exist. 
                // Thus, the triple does not exist
                if(status != 200) {
                    return false;
                }

            }

            InputStream is = response.getEntity().getContent();

            Repository myRepository = new SailRepository(new MemoryStore());
            try {
                myRepository.initialize();
                RepositoryConnection repCon = myRepository.getConnection();
                repCon.add(is, ((KiWiUriResource) subject).getUri(), RDFFormat.N3);
                repCon.commit();

                RepositoryResult<Statement> statements = repCon.getStatements(null, null, null, true);
                Set<KiWiTriple> triples = new HashSet<KiWiTriple>();
                while(statements.hasNext()) {
                    Statement statement = statements.next();
                    String sub_str = statement.getSubject().stringValue();
                    log.debug("sub str: {}", sub_str);
                    KiWiResource subject_resource = new KiWiUriResource(sub_str);

                    String prop_str = statement.getPredicate().stringValue();
                    log.debug("prop str: {}", sub_str);
                    KiWiUriResource property_resource = new KiWiUriResource(prop_str);

                    String obj_str = statement.getObject().stringValue();
                    log.debug("obj str: {}", obj_str);
                    // TODO: if obj str == "..." || obj str == uri::...
                    //KiWiUriResource object_property = new KiWiUriResource(obj_str);
                    // TODO: if obj_str typeOf String/int/Date/etc.
                    KiWiLiteral object_literal = new KiWiStringLiteral(obj_str);

                    String context_str = statement.getContext().stringValue();
                    log.debug("context str: {}", context_str);
                    KiWiUriResource context_resource = new KiWiUriResource(context_str);

                    KiWiTriple t = new KiWiTriple(subject_resource, property_resource, object_literal, context_resource);
                    triples.add(t);
                }

                return triples.contains(wanted_triple);
                
            } catch (RepositoryException e) {
                e.printStackTrace();
            } catch (RDFParseException e) {
                e.printStackTrace();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } catch (HttpException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }

        return false;
    }

    @Override
    public void clearCaches() {
        //Todo: implement cache
    }

}
